package ogmios.core;

import ogmios.components.munches.*;
import java.util.*;
import javax.swing.*;
import java.io.*;
import java.net.*;
import java.util.regex.*;

import ogmios.components.*;

public class CommandManager
{
 private static final Map<String, Class<Command>> loadedCommands = new HashMap<String, Class<Command>>();
 private static final Map<String, AliasedCommand> aliasedCommands = new HashMap<String, AliasedCommand>();


/**
 * Returns an instance of a Command with the given name, or
 * null if the Command Manager cannot find one.<br/>
 * Due to the nature of the CommandManager, calling get(x)
 * will not necessarily return the same thing at different points.
 */
  public static Command get(String name)
 {
   Command command = getAlias(name);
   return command == null ? getCommand(name) : command;
 }

 /**
 * Returns an instance of a Command with the given name, or
 * null if the Command Manager cannot find one.<br/>
 * Due to the nature of the CommandManager, calling get(x)
 * will not necessarily return the same thing at different points.
 */
 @SuppressWarnings("unchecked")
  public static synchronized Command getCommand(String name)
 {
   try
   {
    Class<Command> commandClass = loadedCommands.get(name);
    if (commandClass == null)
    {
     try
     {
      if (new File("commands" + File.separator + name + ".class").exists())
      {
       ClassLoader loader = Ogmios.getMunchLoader();
       Class<?> newClass = loader.loadClass("commands." + name);
       if (Command.class.isAssignableFrom(newClass))
       {
        commandClass = (Class<Command>)newClass;
        put(commandClass);
       }
      }
      else
      {
       return null;
      }
     }
     catch(Throwable ex)
     {
       OgmiosUtilities.showErrorBox(null, ex, "Command:Load Error");
       return null;
     }
    } 
   return commandClass.newInstance();
  }
  catch(Throwable ex)
  {
   OgmiosUtilities.showErrorBox(null, ex, "Command:Create Error");
  }
  return null;
 }

 /**
 * Returns, if it can find one, the AliasCommand for a given name.
 */
 public static synchronized AliasedCommand getAlias(String name)
 {
   AliasedCommand command = aliasedCommands.get(name);
   if (command != null)
    return command;
   File aliasFile = new File("aliases" + File.separator + name + ".oca");
   if (aliasFile.exists() && aliasFile.isFile())
   {
     command = new AliasedCommand(OgmiosUtilities.plainFileContents(aliasFile));
     putAlias(name, command);
     return command;
   }
    return null;
 }

 
 /**
 * Will try to fill up the aliase cache with any alias in the aliases directory
 * not yet cached.
 */
 private static synchronized void fillFromAliases()
 {
   File aliasDirectory = new File("aliases");
   if (aliasDirectory.exists() && aliasDirectory.isDirectory())
   {
      for (File file : aliasDirectory.listFiles(new OgmiosFileFilter(".oca", "Ogmios Command Alias")))
      {
        tryPutAlias(file);
      }
   }
 }
 
 /**
 * Will try to fill up the command cache with any command in the command directory
 * not yet cached.
 */
 private static synchronized void fillFromDirectory()
 {
  File commandsDirectory = new File("commands");
  if (commandsDirectory.exists() && commandsDirectory.isDirectory())
  {
   ClassLoader loader = Ogmios.getMunchLoader();
   for (File file : commandsDirectory.listFiles(new OgmiosFileFilter(".class", "Class Files")))
   {
    tryPut(file, loader);
   }
  }
 }


 /**
 * Tries to put the alias at aliasFile into the cache of aliases.
 * Will not force it in if there is already an alias with the same name there.
 */
 public static synchronized void tryPutAlias(File aliasFile)
 {
   String alias = OgmiosUtilities.unextendedFileName(aliasFile.getName());
   if (getAlias(alias)==null)
   {
     putAlias(alias, OgmiosUtilities.plainFileContents(aliasFile));
   }
 }

 /**
 * Makes, backs up, and creates an alias, replacing any alias already existing.
 */
 public static synchronized void putNewAlias(String alias, String command)
 {
   try
   {
     FileWriter writer  = new FileWriter("aliases" + File.separator + alias + ".oca");
     writer.write(command);
     writer.close();
   }
    catch(IOException ex)
   {
     OgmiosUtilities.showErrorBox(null, ex, "Alias:Write Error");
   }
   putAlias(alias, command);
 }

 /**
 * Tries to put the command at classFile into the cache of commands.
 * Will not force it in if there is already a command with the same name there.
 */
 @SuppressWarnings("unchecked")
 public static synchronized void tryPut(File classFile, ClassLoader loader)
 {
  String name = OgmiosUtilities.unextendedFileName(classFile.getName());
  if (get(name) == null && !name.contains("$"))
  {
    try
   {
     Class<?> newClass = loader.loadClass("commands." + name);
     if (Command.class.isAssignableFrom(newClass))
     {
      put((Class<Command>)newClass);
     }
    }
    catch(ClassNotFoundException ex)
    {
     OgmiosUtilities.showErrorBox(null, ex, "Commands:Load Error");
    }
  }
 }

 /**
 * Puts alias into the CommandManager. This will replace any
 * alias already there
 */
 public static void putAlias(String alias, String commandLine)
 {
   putAlias(alias, new AliasedCommand(commandLine));
 }

 /**
 * Puts alias into the CommandManager. This will replace any
 * alias already there
 */
 public static synchronized void putAlias(String alias, AliasedCommand command)
 {
   aliasedCommands.put(alias, command);
 }

 /**
 * Puts the class into the CommandManager. This will replace any class
 * that is already there. 
 */
 public static void put(Class<Command> commandClass)
 {
  put(commandClass.getSimpleName(), commandClass);
 }

 /**
 * Puts the class into the CommandManager. This will replace any class
 * that is already there. 
 */
 public static synchronized void put(String name, Class<Command> commandClass)
 {
  loadedCommands.put(name, commandClass);
 }

 /**
 * Removes the command with the given name.
 */
 public static synchronized void removeCommand(String name)
 {
  loadedCommands.remove(name);
 }

 /**
 * Removes the named alias from the current cache of aliases.
 */
 public static synchronized void removeAlias(String name)
 {
   removeAlias(name, false);
 }
 
 /**
 * Removes the named alias. If permanently is set to true, the text file backing 
 * the alias is deleted.
 */ 
 public static synchronized void removeAlias(String name, boolean permanently)
 {
   aliasedCommands.remove(name);
   if (permanently)
   {
     File aliasFile = new File("aliases" + File.separator + name + ".oca");
     if (aliasFile.exists())
      aliasFile.delete();
   }
 }
 /**
 * Will try to make a new command from the command directory
 * to replace any copy already in the command cache.
 */
 @SuppressWarnings("unchecked")
 public static synchronized void putNew(String name)
 {
   try
   {
     ClassLoader loader =new NoCacheClassLoader(new URLClassLoader(new URL[]{new File(System.getProperty("user.dir")).toURI().toURL()}), Ogmios.getMunchBlackList());
     Class<?> newCommand = (loader.loadClass("commands." + name));
     if (Command.class.isAssignableFrom(newCommand))
     {
        put(name,(Class<Command>)newCommand);
     }
    }
    catch(Throwable ex)
    {
      OgmiosUtilities.showErrorBox(null, ex, "Class:Load Error"); 
    }
  
  }

 /**
 * Returns a map of all command names to an instance of each command
 */
 public static Map<String, Command> getCommands()
 {
  Map<String, Command> commands = new HashMap<String, Command>();
  fillFromDirectory();
  for (String key : loadedCommands.keySet())
  {
   try
   {
    commands.put(key, loadedCommands.get(key).newInstance());
   }
   catch(Throwable ex)
   {
    OgmiosUtilities.showErrorBox(null, ex, "Commands:Create Error");
   }
  }
  fillFromAliases();
  commands.putAll(aliasedCommands);
  return commands;
 }

 /**
 * Reforms a list of args into a form that, if split by splitCommandLine, would look identical to the list of args
 */
 public static String flattenAndQuoteArguments(List<String> args)
 {
   StringBuilder builder = new StringBuilder();
   for (String arg : args)
   {
     builder.append(quoteCommandLineArgument(arg));
     builder.append(" ");
   }
   int length = builder.length();
   return length == 0 ? "" : builder.substring(0, length - 1);
 }

 /**
 * Quotes an argument so that it would look identical to the original arg if split by splitCommandLine
 */
 public static String quoteCommandLineArgument(String arg)
 {
   StringBuffer buffer = new StringBuffer();
   Matcher matcher;
   for (matcher = Pattern.compile("\\s+|(\")|(\')").matcher(arg); matcher.find();)
   {
     matcher.appendReplacement(buffer, 
     matcher.group(1) != null ? "'\"'" :
     matcher.group(2) != null ? "\"'\"" : 
     "'" + matcher.group() +  "'"
     );
   }
    matcher.appendTail(buffer);
    return buffer.toString();
 }

 public static void main(String[] args)
 {
   checkArg("java foo bear");
   checkArg("java 'fooo\"dfd fd\"dfdfd'");
 }

  public static void checkArg(String commandLine)
  {
    List<String> args = splitCommandLine(commandLine);
    System.out.println(commandLine);
    System.out.println(args);
    String reformed = (flattenAndQuoteArguments(args));
    System.out.println(reformed);
    List<String> reformedArgs = splitCommandLine(reformed);
    System.out.println(reformedArgs);
    System.out.println(reformedArgs.equals(args));
  }

 /**
 * Splits a string into a list, seperated by spaces unless the space are inside quotes, either " or '.
 */
 public static List<String> splitCommandLine(String args)
 {
  List<String> arguments = new ArrayList<String>();
  int length = args.length();
  boolean inWord = false;
  int start = 0;
  StringBuilder tokenBuilder = new StringBuilder();
  for (int i = 0; i < length; i++)
  {
    char c = args.charAt(i);
    if (Character.isWhitespace(c))
    {
      if (inWord)
      {
        tokenBuilder.append(args.substring(start, i));
        arguments.add(tokenBuilder.toString());
        tokenBuilder.setLength(0);
        inWord = false;
      }
    }
    else
    {
      if (!inWord)
      {
        inWord = true;
        start = i;
      }
      quoteSwitch:
      switch(c)
      {
        case '\'':
          tokenBuilder.append(args.substring(start,i));
          start = i + 1;
          for (i=i+1;i<length; i++)
          {
            c = args.charAt(i);
            if (c == '\'')
            {
              tokenBuilder.append(args.substring(start, i));
              start = i + 1;
              break quoteSwitch;
            }
          }
         return null;
        case '\"':
         tokenBuilder.append(args.substring(start,i));
         start = i + 1;
          for (i=i+1;i<length; i++)
          {
            c = args.charAt(i);
            if (c == '"' )
            {
              tokenBuilder.append(args.substring(start, i));
              start = i + 1;
              break quoteSwitch;
            }
          }
         return null;
      }
    }
  }
  if (inWord)
  {
    arguments.add(tokenBuilder.toString() + args.substring(start));
  }
  return arguments;
 }

 /**
 * Returns a set of all the loaded commands' names.
 * No guarentee is given on their order.
 */
 public static Set<String> getCommandNames()
 {
  fillFromDirectory();
  fillFromAliases();
  if (aliasedCommands.isEmpty())
   return loadedCommands.keySet();
  else
  {
    Set<String> commandNames = new HashSet<String>();
    commandNames.addAll(loadedCommands.keySet());
    commandNames.addAll(aliasedCommands.keySet());
    return commandNames;
  }
 }

 public static void execute(String args)
 {
  execute(args, null);
 }

 public static void execute(String arguments, Context shell)
 {
  List<String> args = splitCommandLine(arguments);
  if (args==null)
  {
    shell.println("Bad input.");
  }
  else if (args.size() > 0)
  {
   String commandName = args.get(0);
   Command command = get(commandName);
   if (command == null)
   {
    if (shell != null)
    {
     shell.print("There is no command called ");
     shell.println(commandName);
    }
   }
   else
   {
     args.remove(0);
     execute(command, args, shell);
   }
  }
 }

 public static void execute(Command command, List<String> args)
 {
  execute(command, args, null);
 }

 public static void execute(Command command, List<String> args, Context shell)
 {
  if (command != null)
  {
   try
   {
    command.execute(shell == null ? Context.EMPTY_CONTEXT : shell, args == null ? Collections.<String>emptyList() : args);
   }
   catch(Throwable ex)
   {
     shell.println("Error occured executing " + command.getClass().getSimpleName());
   }
  }
 }

 public static class AliasedCommand implements Command, Chainable
 {
   private final String alias;

   public AliasedCommand(String alias)
   {
     this.alias = alias;
   }

    public String getManPage()
    {
      Command command = getCommand();
       return command == null ? "Cannot locate command in \n" + alias:
       "This alias is backed by the following command: \n" + alias + "\n" + command.getManPage();
    }

    public Command getCommand()
    {
      List<String> args = splitCommandLine(alias);
      if (args == null)
       return null;
      String name;
      Command command =   CommandManager.get(name=args.get(0));
       //To prevent an infinite looping of aliases.
      if (command instanceof AliasedCommand && ((AliasedCommand)command).alias.equals(alias))
     {
      command = CommandManager.getCommand(name);
     }
     return command;
    }

    public String getCommandString()
    {
      return alias;
    }

    public void execute(Context shell, List<String> args) throws Throwable
    {
      execute(shell, args, new ChainableListener[0]);
    }

    public void execute(Context shell, List<String> args, ChainableListener... listeners) throws Throwable
    {
      List<String> aliasArgs = splitCommandLine(alias);
      if (aliasArgs == null)
       throw new Exception("Could not split command " + alias);
      if (aliasArgs.size() == 0)
       throw new Exception("There are no commands in the alias " + alias);
      String commandName;
      Command command = CommandManager.get(commandName=aliasArgs.get(0));
      if (command == null)
       throw new Exception("There is no command called " + commandName);

      //To prevent an infinite looping of aliases.
      if (command instanceof AliasedCommand && ((AliasedCommand)command).alias.equals(alias))
     {
      command = CommandManager.getCommand(commandName=aliasArgs.get(0));
      if (command == null)
       throw new Exception("There is no command called " + commandName);
     }
      aliasArgs.remove(0);
      aliasArgs.addAll(args);
      if (command instanceof Chainable)
      {
        ((Chainable)command).execute(shell, aliasArgs, listeners); 
       }
       else
      {
       command.execute(shell, aliasArgs);
      }
    } 
 }

}